#ifndef _XML_NODES_HPP_
#define _XML_NODES_HPP_

#include <fstream>
#include <string>
#include <stack>
#include <vector>
#include <unordered_map>
#include <sstream>
#include <iostream>


#define ROOT_NODE_NAME "__ROOT__"

#define EMPTY_NODE_NAME "__UNNAMED__"


namespace XMLReader{
	class __declspec(dllexport) XMLNode;
	using ChildrensContainerType = std::vector<XMLNode>;
	using AttributesContainerType = std::unordered_map<std::string, std::string>;

	// boost unordered_flat_map and boost fast_allocator could be used for even more speedup...

	class __declspec(dllexport) XMLNode
	{
		std::string name;

		std::string value;

		ChildrensContainerType children;

		AttributesContainerType attributes;


		static std::pair<std::string, AttributesContainerType> ParseTag(std::string tag){
			const auto notWhiteSpace = [](const auto c) { return not std::isspace(c); };
			tag.erase(tag.begin(), std::find_if(tag.begin(), tag.end(), notWhiteSpace));
			const auto itFirstWhite = std::find_if(tag.begin(), tag.end(), [](const auto c) { return std::isspace(c); });
			const auto tagName = tag.substr(0, std::distance(tag.begin(), itFirstWhite));
			tag.erase(tag.begin(), itFirstWhite);
			tag.erase(tag.begin(), std::find_if(tag.begin(), tag.end(), notWhiteSpace));
			bool inQuotes = false;
			std::size_t startNameIndex = 0, startValueIndex = 0;
			std::string name;

			AttributesContainerType attributes;

			for (std::size_t index = 0; index < tag.size(); ++index){
				const auto c = tag[index];

				if (inQuotes) {
					if (c == '"' and index and tag[index - 1] not_eq '\\') {
						attributes.insert_or_assign(name, tag.substr(startValueIndex, index - startValueIndex));
						inQuotes = false;
					}
				}
				else{
					if (std::isspace(c))
						continue;

					else if (c == ',')
						startNameIndex = index + 1;

					else if (c == '='){
						name = tag.substr(startNameIndex, index - startNameIndex);
						name.erase(name.begin(), std::find_if(name.begin(), name.end(), notWhiteSpace));
						name.erase(std::find_if(name.rbegin(), name.rend(), notWhiteSpace).base(), name.end());
					}

					else if (c == '"' and index and tag[index - 1] not_eq '\\'){
						startValueIndex = index + 1;
						inQuotes = true;
					}
				}
			}

			return { tagName, attributes };
		}


	public:

		explicit XMLNode(const std::string& nameArg = "") :
			name{ nameArg.empty() ? EMPTY_NODE_NAME : nameArg }, value{}, children{}, attributes{} {
		}

		XMLNode(const std::string& nameArg, const std::string& valueArg, const AttributesContainerType& attrs) :
			name{ nameArg }, value{ valueArg }, children{}, attributes{ attrs } {
		}

		void SetName(const std::string& nameArg) { this->name = nameArg; }

		std::string GetName(void) const { return this->name; }

		void SetValue(const std::string& valueArg) { this->value = valueArg; }

		std::string GetValue(void) const { return this->value; }

		void SetAttributes(const AttributesContainerType& attrs) { this->attributes = attrs; }

		AttributesContainerType GetAttributes(void) { return this->attributes; }

		const AttributesContainerType& GetConstRefAttributes(void) const noexcept { return this->attributes; }

		void ClearAttributes(void) { this->attributes.clear(); }

		std::string GetAttribute(const std::string& name)
		{
			try { return this->attributes.at(name); }
			catch (...) { return ""; }
		}

		void SetAttribute(const std::string& name, const std::string& value)
		{
			this->attributes.insert_or_assign(name, value);
		}

		void ClearChildren(void) { this->children.clear(); }

		void SetChildren(const ChildrensContainerType& childrenArg) { this->children = childrenArg; }

		std::size_t GetChildrenCount(void) const { return this->children.size(); }

		const ChildrensContainerType& GetChildrenConstRef(void) const { return this->children; }

		ChildrensContainerType& GetChildrenRef(void) noexcept { return this->children; }

		XMLNode& GetChildRefByIndex(const std::size_t index) { return this->children.at(index); }

		const XMLNode& GetChildConstRefByIndex(const std::size_t index) const { return this->children.at(index); }

		XMLNode* GetChildPtrByName(const std::string& name, std::size_t index = 1){
			index = std::max(index, (decltype(index))1);
			auto it = std::find_if(this->children.begin(), this->children.end(), [&name, &index](const auto& x) { return x.GetName() == name and --index == 0; });
			return it not_eq this->children.end() ? std::addressof(*it) : nullptr;
		}

		void AddChild(const XMLNode& nodeArg) { this->children.emplace_back(nodeArg); }

		XMLNode& AddChild(const std::string& name, const std::string& value = "", const AttributesContainerType& attrs = {})
		{
			return this->children.emplace_back(name, value, attrs);
		}

		void RemoveChildByIndex(std::size_t index)
		{
			if (index)
				--index;

			if (index < this->children.size())
				this->children.erase(this->children.begin() + index);
		}

		void RemoveChildByName(const std::string& name, std::size_t index = 1)
		{
			index = std::max(index, (decltype(index))1);

			const auto it = std::find_if(this->children.begin(), this->children.end(),
				[&name, &index](const auto& x) { return x.GetName() == name and --index == 0; });

			if (it not_eq this->children.end())
				this->children.erase(it);
		}

		std::string ToString(void) const
		{
			std::stringstream ss;

			XMLNode::PrintXML(*this, 0, ss);

			return ss.str();
		}

		void FromString(const std::string& xml)
		{
			this->value.clear();

			this->children = XMLNode::ParseXML(xml).GetChildrenConstRef();
		}

		bool ToFile(const std::string& filePath)
		{
			std::ofstream filestream(filePath);

			if (not filestream.is_open())
				return false;

			XMLNode::PrintXML(*this, 0, filestream);

			return true;
		}

		bool FromFile(const std::string& filePath)
		{
			std::ifstream filestream(filePath);

			if (not filestream.is_open())
				return false;

			this->value.clear();

			this->children = XMLNode::ParseXML(std::string((std::istreambuf_iterator<char>(filestream)), (std::istreambuf_iterator<char>()))).GetChildrenConstRef();

			return true;
		}

		void Print(void) { XMLNode::PrintXML(*this); }

		static void PrintXML(const XMLNode& node, std::uint16_t tabsArg = 0, std::ostream& out = std::cout)
		{
			out << std::string(tabsArg, '\t') << "<" << node.GetName();

			std::size_t index = 0;

			const auto& attrs = node.GetConstRefAttributes();

			for (const auto& [name, value] : attrs)
			{
				out << " " << name << "=\"" << value << "\"";

				if (++index not_eq attrs.size())
					out << ",";
			}

			out << ">";

			if (node.GetChildrenCount())
			{
				out << "\n";

				for (const auto& childNode : node.GetChildrenConstRef())
					PrintXML(childNode, tabsArg + 1, out);
			}

			else
				out << node.GetValue();

			out << std::string(node.GetChildrenCount() ? tabsArg : 0, '\t') << "</" << node.GetName() << ">";

			out << "\n";
		}

		static XMLNode ParseXML(const std::string& xml)
		{
			try
			{
				XMLNode root{ ROOT_NODE_NAME };

				XMLNode* current = std::addressof(root);

				std::stack<XMLNode*> nodes{ { current } };

				bool openingTag = false;

				std::string value;

				for (std::size_t index{ 0 }; index < xml.size(); )
				{
					if (const auto c = xml[index]; not std::isspace(c))
					{
						if (c == '<')
						{
							const std::size_t start = index + 1, end = xml.find_first_of(">", start);

							if (xml[start] == '/')
							{
								if (XMLNode* const node = nodes.top(); xml.substr(start + 1, end - start - 1) == node->GetName())
								{
									if (node->GetChildrenCount() == 0)
									{
										node->SetValue(value);

										value.clear();
									}

									nodes.pop();
								}

								else
									return XMLNode{};

								current = nodes.top();

								openingTag = false;
							}

							else
							{
								const auto [tagName, attrs] = XMLNode::ParseTag(xml.substr(start, end - start));

								nodes.emplace((current = std::addressof(current->AddChild(tagName, "", attrs))));

								openingTag = true;
							}

							index = end;

							continue;
						}

						else if (c == '>' and openingTag)
						{
							const std::size_t start = index + 1, end = xml.find_first_of("<", start);

							value = xml.substr(start, end - start);

							index = end;

							continue;
						}
					}

					++index;
				}

				return root;
			}

			catch (...)
			{
				return XMLNode{};
			}
		}
	};

}

#endif
